《夏洛特烦恼》

撰文 | 乌其多

审校 | 刘六七

‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍众所不周知,大部分编程语言都算不出0.1+0.2=0.3。

比如Javascript一通计算下的结果就为:0.30000000000000004!

cloud.tencent

你可能会说:“你先等会儿!”

然后打开windows上自带的计算器,输入“0.1+0.2”,然后拿着计算结果抡圆了准备打我的小脸。

自制

其实,计算器看似正确的结果,只是计算器把存在精度问题的最后几位在显示的时候舍入掉了而已。我们可以理解为这是一种“障眼法”。

自制

那问题到底出在哪儿?

要理解这个现象,得先搞明白计算机是怎么看待小数的。

我们平时用的是十进制,但计算机只懂二进制,也就是那一堆0和1。把十进制小数转成二进制,用的是“乘2取整”法。以 0.1 为例:

自制

看出来了吗?0.1 的二进制是无限循环的:0.00011001100110011……(0011 循环)。

同样,0.2 的二进制也是无限循环:0.0011001100110011……(0011 循环)。

看不出来也不要紧,我们只要记住:我们十进制世界里大部分稀松平常的小数,二进制都是无法精准表达的。

0.1 和 0.2在二进制世界里,就是“无限循环小数”,也就如同十进制无法精确表示 1/3 一样。

除了0.1和0.2以外,π、√2等数学上的无理数(无限不循环小数),二进制下更是超纲了,计算机同样也是无法精确表示的。

弄懂了这个概念,我们再来看计算机的存储法则。

我们现在主流的编程语言,通用的存储法基本都是“IEEE 754浮点数标准”,只要是使用了这个存储标准的语言,比如JavaScript、C/C++、Java、以及曾号称人人都应该掌握的Python等等,都是无法准确计算出“0.1+0.2=0.3”的。

自制

IEEE 754是由电气与电子工程师协会(IEEE)于1985年制定的浮点运算技术标准。

在这部“数字宪法”制定之前,各大计算机厂家都有自己的浮点数存储法则,你不兼容我的,我不care你的,主打一个各论各的,造成了很多混乱。

IEEE 754标准就规定了浮点数在计算机中的存储格式,常见的有32位单精度和64位双精度。每个数被拆分为符号位、指数位和尾数位三部分,尾数位长度是固定的(如双精度为52位),那么遇到无限循环或者无限不循环小数的时候,怎么办?那就是把尾数“一剪没”了。

这样截断或舍入,就给0.1和0.2造成了存储误差。两个近似值相加,误差累积,就导致了0.1+0.2≠0.3的现象。

梅开二度、二度返场|《夏洛特烦恼》

你可能又得问:浮点数存储为啥非得把尾数截了?计算机这么牛,全存了不行吗?

答案很简单:不是不想,是做不到。

很多人以为计算机存储和计算都是绝对精确的,其实这是个误解。

存整数确实可以——比如数字 42,转成二进制 101010,放进固定大小的空间里,只要空间够大(不溢出),拿出来还是 42,分毫不差。就像把鸡蛋放进仓库,拿出来还是那个鸡蛋。

但问题是,数学世界里的数远不止如此。无限循环和无限不循环的数,远远多于整数。

计算机的存储空间是有限的,无论用多少位,都装不下一个无限长的二进制串。所以,它只能截取前几十位,后面的直接扔掉。

你可能又会想:那能不能把空间做得再大点,存更多位?从几十位变成几百位也行啊!

理论上可以,但实际意义不大。

对绝大多数工程、图形、科学计算来说,“IEEE 754浮点数存储法”所用的 64 位双精度(约 15-17 位有效数字)已经足够精确。

对不同领域来说,计算精度要求是不同的,天文学家能算准光年就很牛了|网络

如设计飞机、渲染电影、模拟天气,没人需要知道 π 的第10000位是什么(当然,π能不能算到最后一位,在数学上意义重大,可能意味着数学大厦的地基比如微积分、极限理论被动摇)。况且,无理数有无穷多个,再大的空间也装不完。

所以计算机选择了一种“务实”的策略:用有限的存储,近似地表示所有的实数。专业的相关学科就叫做计算机数值分析,研究的就是“近似”的艺术。

对于任何一个数值计算问题,数值分析都会追问:误差有多大?算法快不快?结果稳不稳?能不能算出来?只要这四个问题都能给出满意的答案,那就是合格的计算结果。

回到 0.1 + 0.2。这不是程序的 bug,而是二进制与有限存储共同作用的必然结果。

理解这个“误差”,就是数值分析教给我们的最重要一课:

认识误差,理解误差,最终学会与误差共存,并控制它。

参考资料:

[1]https://bbs.huaweicloud.com/blogs/287063

[2]https://www.puntoflotante.net/FLOATING-POINT-FORMAT-IEEE-754.htm

[3]https://www.h-schmidt.net/FloatConverter/IEEE754.html